/*    ______   ___    ___
*   /\  _   \ /\_ \  /\_ \
*   \ \ \L\ \\//\ \ \//\ \      __     __  _ __ ___
*    \ \  __ \ \ \ \  \ \ \   /'__`\ /'_ `\/\`'__\/ __`\
*     \ \ \/\ \ \_\ \_ \_\ \_/\  __//\ \L\ \ \ \//\ \L\ \
*      \ \_\ \_\/\____\/\____\ \____\ \____ \ \_\\ \____/
*       \/_/\/_/\/____/\/____/\/____/\/___L\ \/_/ \/___/
*                  /\____/
*                  \_/__/
*
*      MacOS X HID Manager access routines.
*
*      By Angelo Mottola.
*
*      See readme.txt for copyright information.
*/


#include "allegro5/allegro.h"
#include "allegro5/internal/aintern.h"
#include "allegro5/internal/aintern_events.h"
#include "allegro5/internal/aintern_keyboard.h"
#include "allegro5/platform/aintosx.h"

#ifndef ALLEGRO_MACOSX
#error Something is wrong with the makefile
#endif

#define USAGE(p, u) (((p)<<16)+(u))

static void hid_store_element_data(CFTypeRef element, int type, HID_DEVICE *device, int app, int collection, int index);
static void hid_scan_physical_collection(CFTypeRef properties, HID_DEVICE *device, int app, int collection);
static void hid_scan_application_collection(CFMutableDictionaryRef properties, HID_DEVICE *device);

static HID_DEVICE* add_device(HID_DEVICE_COLLECTION*);

/* add_element:
* Add a new, blank element to a device.
* This is later specialized into a button
* axis or whatever
*/
static HID_ELEMENT* add_element(HID_DEVICE* d) {
   HID_ELEMENT* e;
   if (d->element==NULL) {
      d->capacity=8;
      d->element=al_malloc(d->capacity*sizeof(HID_ELEMENT));
   }
   if (d->num_elements>=d->capacity) {
      d->capacity*=2;
      d->element=al_realloc(d->element, d->capacity*sizeof(HID_ELEMENT));
   }
   e=&d->element[d->num_elements++];
   memset(e, 0, sizeof(HID_ELEMENT));
   return e;
}

/* i_val:
* Helper method - get an integer value from a dictionary
* Defaults to 0 if the value is not found; in practice this
* should not occur.
*/
static int i_val(CFTypeRef d, CFStringRef key) {
   int ans;
   CFTypeRef num = CFDictionaryGetValue(d, key);
   if (num)
      CFNumberGetValue(num, kCFNumberIntType, &ans);
   else
      ans = 0;
   return ans;
}

/* hid_store_element_data:
* Parse this HID element, break it down and store the
* relevant data in the device structure
*/
static void hid_store_element_data(CFTypeRef element, int type, HID_DEVICE *device, int app, int col, int idx)
{
   HID_ELEMENT *hid_element;
   CFTypeRef type_ref;
   const char *name;

   hid_element = add_element(device);
   hid_element->type = type;
   hid_element->index = idx;
   hid_element->col = col;
   hid_element->app = app;
   hid_element->cookie = (IOHIDElementCookie) i_val(element, CFSTR(kIOHIDElementCookieKey));
   hid_element->min = i_val(element, CFSTR(kIOHIDElementMinKey));
   hid_element->max = i_val(element, CFSTR(kIOHIDElementMaxKey));
   type_ref = CFDictionaryGetValue(element, CFSTR(kIOHIDElementNameKey));
   if ((type_ref) && (name = CFStringGetCStringPtr(type_ref, CFStringGetSystemEncoding())))
      hid_element->name = strdup(name);
   else
      hid_element->name = NULL;
}

/* hid_scan_application:
* scan the elements that make up one 'application'
* i.e. one unit like a joystick.
*/
static void hid_scan_application(CFTypeRef properties, HID_DEVICE *device, int app)
{
   CFTypeRef array_ref, element;
   int type, usage_page, usage;
   int i;
   int axis=0;
   int stick=0;
   array_ref = CFDictionaryGetValue(properties, CFSTR(kIOHIDElementKey));
   if ((array_ref) && (CFGetTypeID(array_ref) == CFArrayGetTypeID())) {
      for (i = 0; i < CFArrayGetCount(array_ref); i++) {
         element = CFArrayGetValueAtIndex(array_ref, i);
         if (CFGetTypeID(element) == CFDictionaryGetTypeID()) {
            type = i_val(element, CFSTR(kIOHIDElementTypeKey));
            usage = i_val(element, CFSTR(kIOHIDElementUsageKey));
            usage_page = i_val(element, CFSTR(kIOHIDElementUsagePageKey));
            if (type == kIOHIDElementTypeCollection)
            {
               /* It is a collection; recurse into it, if it is part of the
                * generic desktop (sometimes the whole joystick is wrapped
                * up inside a collection like this.
                */
               if (usage_page == kHIDPage_GenericDesktop)
                  hid_scan_application(element, device, app);
            }
            else {
               switch (usage_page) {
                  case kHIDPage_GenericDesktop:
                     switch (usage) {
                        case kHIDUsage_GD_Pointer:
                           if (axis!=0) {
                              /* already have some elements in this stick */
                              ++stick;
                              axis=0;
                           }
                           hid_scan_physical_collection(element, device, app, stick);
                           ++stick;
                           break;
                        case kHIDUsage_GD_X:
                           hid_store_element_data(element, HID_ELEMENT_AXIS_PRIMARY_X, device, app, stick, axis++);
                           break;

                        case kHIDUsage_GD_Y:
                           hid_store_element_data(element, HID_ELEMENT_AXIS_PRIMARY_Y, device, app, stick, axis++);
                           break;

                        case kHIDUsage_GD_Z:
                        case kHIDUsage_GD_Rx:
                        case kHIDUsage_GD_Ry:
                        case kHIDUsage_GD_Rz:
                           hid_store_element_data(element, HID_ELEMENT_AXIS, device,app, stick, axis++);
                           break;

                        case kHIDUsage_GD_Slider:
                        case kHIDUsage_GD_Dial:
                        case kHIDUsage_GD_Wheel:
                           /* If we've already seen some axes on this stick, move to the next one */
                           if (axis > 0)
                           {
                              ++stick;
                              axis=0;
                           }
                           hid_store_element_data(element, HID_ELEMENT_STANDALONE_AXIS, device, app, stick++, 0);
                           break;

                        case kHIDUsage_GD_Hatswitch:
                           /* If we've already seen some axes on this stick, move to the next one */
                           if (axis > 0)
                           {
                              ++stick;
                              axis=0;
                           }
                           hid_store_element_data(element, HID_ELEMENT_HAT, device, app, stick++, 0);
                           break;
                     }
                     break;
                  case kHIDPage_Button:
                     hid_store_element_data(element, HID_ELEMENT_BUTTON, device, app, 0, usage-1);
                     break;
               }
            }
            if (axis>=2) {
               ++stick;
               axis=0;
            }
         }
      }
   }
}

/* hid_scan_physical_collection:
* scan the elements that make up one 'stick'
*/
static void hid_scan_physical_collection(CFTypeRef properties, HID_DEVICE *device, int app, int stick)
{
   CFTypeRef array_ref, element;
   int type, usage_page, usage;
   int i;
   int axis=0;
   array_ref = CFDictionaryGetValue(properties, CFSTR(kIOHIDElementKey));
   if ((array_ref) && (CFGetTypeID(array_ref) == CFArrayGetTypeID())) {
      for (i = 0; i < CFArrayGetCount(array_ref); i++) {
         element = CFArrayGetValueAtIndex(array_ref, i);
         if (CFGetTypeID(element) == CFDictionaryGetTypeID()) {
            type = i_val(element, CFSTR(kIOHIDElementTypeKey));
            usage = i_val(element, CFSTR(kIOHIDElementUsageKey));
            usage_page = i_val(element, CFSTR(kIOHIDElementUsagePageKey));
            switch (usage_page) {
               case kHIDPage_GenericDesktop:
                  switch (usage) {
                     case kHIDUsage_GD_X:
                        hid_store_element_data(element, HID_ELEMENT_AXIS_PRIMARY_X, device, app, stick, axis++);
                        break;

                     case kHIDUsage_GD_Y:
                        hid_store_element_data(element, HID_ELEMENT_AXIS_PRIMARY_Y, device, app, stick, axis++);
                        break;

                     case kHIDUsage_GD_Z:
                     case kHIDUsage_GD_Rx:
                     case kHIDUsage_GD_Ry:
                     case kHIDUsage_GD_Rz:
                        hid_store_element_data(element, HID_ELEMENT_AXIS, device,app, stick, axis++);
                        break;
                  }
                  break;
               case kHIDPage_Button:
                  hid_store_element_data(element, HID_ELEMENT_BUTTON, device, app, 0, usage-1);
                  break;
            }
         }
      }
   }
}

/* hid_scan_application_collection:
* Scan the elements array; each element will be an 'application'
* i.e. one joystick, gamepad or mouse
*
*/
static void hid_scan_application_collection(CFMutableDictionaryRef properties, HID_DEVICE *device)
{
   CFTypeRef array_ref, element;

   int usage, usage_page;
   int i;

   array_ref = CFDictionaryGetValue(properties, CFSTR(kIOHIDElementKey));
   if ((array_ref) && (CFGetTypeID(array_ref) == CFArrayGetTypeID())) {
      for (i = 0; i < CFArrayGetCount(array_ref); i++) {
         element = CFArrayGetValueAtIndex(array_ref, i);
         if (CFGetTypeID(element) == CFDictionaryGetTypeID()) {
            usage=i_val(element, CFSTR(kIOHIDElementUsageKey));
            usage_page=i_val(element, CFSTR(kIOHIDElementUsagePageKey));
            switch (USAGE(usage_page,  usage)) {
               case USAGE(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick):
                  device->type=HID_JOYSTICK;
                  hid_scan_application(element, device, device->cur_app);
                  device->cur_app++;
                  break;
               case USAGE(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad):
                  device->type=HID_GAMEPAD;
                  hid_scan_application(element, device, device->cur_app);
                  device->cur_app++;
                  break;
               case USAGE(kHIDPage_GenericDesktop, kHIDUsage_GD_Mouse):
                  device->type=HID_MOUSE;
                  hid_scan_application(element, device, device->cur_app);
                  device->cur_app++;
                  break;
            }
         }
      }
   }
}

/* get_usb_properties:
* Get a property dictionary from the USB plane.
*/
static CFMutableDictionaryRef get_usb_properties(io_object_t device)
{
   io_registry_entry_t parent, grandparent;
   CFMutableDictionaryRef usb_properties = NULL;
   if (IORegistryEntryGetParentEntry(device, kIOServicePlane, &parent) == KERN_SUCCESS) {
      if (IORegistryEntryGetParentEntry(parent, kIOServicePlane, &grandparent) == KERN_SUCCESS) {
         IORegistryEntryCreateCFProperties (grandparent, &usb_properties,
                                    kCFAllocatorDefault, kNilOptions);
         IOObjectRelease(grandparent);
      }
      IOObjectRelease(parent);
   }
   return usb_properties;
}

#if OSX_HID_PSEUDO_SCAN
/*
 * Pseudo scan - for development purposes, if someone has hardware
 * that isn't parsed by this code, you can ask them to dump _their_ scan
 * as a plist, then reload it here in order to debug it.
 */
HID_DEVICE_COLLECTION *_al_osx_hid_scan(int type, HID_DEVICE_COLLECTION* col)
{
   HID_DEVICE* this_device;
   NSDictionary* properties = nil;
   NSString* filename;
   switch (type) {
      case HID_GAMEPAD:
         filename = @"gamepad.plist";
         break;
      case HID_JOYSTICK:
         filename = @"joystick.plist";
         break;
      case HID_MOUSE:
         filename = @"mouse.plist";
         break;
      default:
         filename = nil;
         break;
   }
   if (filename != nil)
      properties = [NSDictionary dictionaryWithContentsOfFile:filename];
   if (properties)
   {
      this_device = add_device(col);
      this_device->manufacturer = strdup([((NSString*) [properties objectForKey: @kIOHIDManufacturerKey]) lossyCString]);
      this_device->product = strdup([((NSString*) [properties objectForKey: @kIOHIDProductKey]) lossyCString]);
      hid_scan_application_collection((CFDictionaryRef) properties, this_device);
      [properties release];
   }
   return col;
}
#else
/* _get_matching_services:
 * get iterator for corresponding services
 */
static IOReturn _get_matching_services(mach_port_t master_port, int usage_page, int usage, io_iterator_t *hid_object_iterator)
{
   CFMutableDictionaryRef class_dictionary = NULL;
   CFNumberRef usage_ref = NULL;
   CFNumberRef usage_page_ref = NULL;

   class_dictionary = IOServiceMatching(kIOHIDDeviceKey);
   if (class_dictionary) {
   /* Add key for device type to refine the matching dictionary. */
   usage_ref = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage);
   usage_page_ref = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage_page);
   CFDictionarySetValue(class_dictionary, CFSTR(kIOHIDDeviceUsageKey), usage_ref);
   CFDictionarySetValue(class_dictionary, CFSTR(kIOHIDDeviceUsagePageKey), usage_page_ref);
   }
   IOReturn result = IOServiceGetMatchingServices(master_port, class_dictionary, hid_object_iterator);
   if (usage_ref) CFRelease(usage_ref);
   if (usage_page_ref) CFRelease(usage_page_ref);
   return result;
}

/* _al_osx_hid_scan:
* Scan the hid manager for devices of type 'type',
* and append to the collection col
*/
HID_DEVICE_COLLECTION *_al_osx_hid_scan(int type, HID_DEVICE_COLLECTION* col)
{
   ASSERT(col);
   HID_DEVICE *this_device;
   mach_port_t master_port = 0;
   io_iterator_t hid_object_iterator = 0;
   io_object_t hid_device = 0;
   int usage, usage_page;
   CFTypeRef type_ref;
   CFMutableDictionaryRef properties = NULL, usb_properties = NULL;
   IOCFPlugInInterface **plugin_interface = NULL;
   IOReturn result;
   const char *string;
   SInt32 score = 0;
   int error;

   usage_page = kHIDPage_GenericDesktop;
   switch (type) {
      case HID_MOUSE:
         usage = kHIDUsage_GD_Mouse;
         break;
      case HID_JOYSTICK:
         usage = kHIDUsage_GD_Joystick;
         break;
      case HID_GAMEPAD:
         usage=kHIDUsage_GD_GamePad;
         break;
   }

   result = IOMasterPort(bootstrap_port, &master_port);
   if (result == kIOReturnSuccess) {
      result = _get_matching_services(master_port, usage_page, usage, &hid_object_iterator);
      if ((type == HID_MOUSE) && (hid_object_iterator == IO_OBJECT_NULL)) {
         /* in case of a mouse, GD_Mouse must not be true but can also be a pointing device */
         result = _get_matching_services(master_port, usage_page, kHIDUsage_GD_Pointer, &hid_object_iterator);
      }
      if ((result == kIOReturnSuccess) && (hid_object_iterator)) {
         /* Ok, we have a list of attached HID devices; scan them. */
         while ((hid_device = IOIteratorNext(hid_object_iterator))!=0) {
            if ((IORegistryEntryCreateCFProperties(hid_device, &properties, kCFAllocatorDefault, kNilOptions) == KERN_SUCCESS) && (properties != NULL)) {
               error = false;
               this_device = add_device(col);
               this_device->type = type;
               if (col->count==0) {
                  this_device->cur_app=0;
               }
               else {
                  this_device->cur_app=col->devices[col->count-1].cur_app;
               }
               /*
                * Mac OS X currently is not mirroring all USB properties
                * to HID page so need to look at USB device page also.
                */
               usb_properties = get_usb_properties(hid_device);
               type_ref = CFDictionaryGetValue(properties, CFSTR(kIOHIDManufacturerKey));
               if (!type_ref)
                  type_ref = CFDictionaryGetValue(usb_properties, CFSTR("USB Vendor Name"));
               if ((type_ref) && (string = CFStringGetCStringPtr(type_ref, CFStringGetSystemEncoding())))
                  this_device->manufacturer = strdup(string);
               else
                  this_device->manufacturer = NULL;
               type_ref = CFDictionaryGetValue(properties, CFSTR(kIOHIDProductKey));
               if (!type_ref)
                  type_ref = CFDictionaryGetValue(usb_properties, CFSTR("USB Product Name"));
               if ((type_ref) && (string = CFStringGetCStringPtr(type_ref, CFStringGetSystemEncoding())))
                  this_device->product = strdup(string);
               else
                  this_device->product = NULL;

               type_ref = CFDictionaryGetValue(usb_properties, CFSTR("USB Address"));
               if ((type == HID_MOUSE) && (!type_ref)) {
                  /* Not an USB mouse. Must be integrated trackpad: we report it as a single button mouse */
                  add_element(this_device)->type = HID_ELEMENT_BUTTON;
               }
               else {
                  /* Scan for device elements */
                  this_device->num_elements = 0;
                  hid_scan_application_collection(properties, this_device);
               }

               this_device->interface = NULL;
               if ((type == HID_JOYSTICK) || (type == HID_GAMEPAD)) {
                  /* Joystick or gamepad device: create HID interface */
                  if (IOCreatePlugInInterfaceForService(hid_device, kIOHIDDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &plugin_interface, &score) != kIOReturnSuccess)
                     error = true;
                  else {
                     if ((*plugin_interface)->QueryInterface(plugin_interface, CFUUIDGetUUIDBytes(kIOHIDDeviceInterfaceID), (void *)&(this_device->interface)) != S_OK)
                        error = true;
                     (*plugin_interface)->Release(plugin_interface);
                     if ((*(this_device->interface))->open(this_device->interface, 0) != KERN_SUCCESS)
                        error = true;
                  }
               }
               if (error) {
                  if (this_device->manufacturer)
                     al_free(this_device->manufacturer);
                  if (this_device->product)
                     al_free(this_device->product);
                  if (this_device->interface)
                     (*(this_device->interface))->Release(this_device->interface);
                  this_device->interface=NULL;
                  --col->count;
               }

               CFRelease(properties);
               CFRelease(usb_properties);
            }
         }
         IOObjectRelease(hid_object_iterator);
      }
      mach_port_deallocate(mach_task_self(), master_port);
   }

   return col;
}
#endif
/* add_device:
* add a new device to a collection
*/
static HID_DEVICE* add_device(HID_DEVICE_COLLECTION* o) {
   HID_DEVICE* d;
   if (o->devices==NULL) {
      o->count=0;
      o->capacity=1;
      o->devices=al_malloc(o->capacity*sizeof(HID_DEVICE));
   }
   if (o->count>=o->capacity) {
      o->capacity*=2;
      o->devices=al_realloc(o->devices, o->capacity*sizeof(HID_DEVICE));
   }
   d=&o->devices[o->count];
   memset(d, 0, sizeof(HID_DEVICE));
   /* Chain onto the preceding app count */
   if (o->count>0)
      d->cur_app = o->devices[o->count-1].cur_app;
   ++o->count;
   return d;
}

/* _al_osx_hid_free:
* Release the memory taken up by a collection
*/
void _al_osx_hid_free(HID_DEVICE_COLLECTION *col)
{
   HID_DEVICE *device;
   HID_ELEMENT *element;
   int i, j;

   for (i = 0; i < col->count; i++) {
      device = &col->devices[i];
      if (device->manufacturer)
         al_free(device->manufacturer);
      if (device->product)
         al_free(device->product);
      for (j = 0; j < device->num_elements; j++) {
         element = &device->element[j];
         if (element->name)
            al_free(element->name);
      }
      al_free(device->element);
      if (device->interface) {
         (*(device->interface))->close(device->interface);
         (*(device->interface))->Release(device->interface);
      }
   }
   al_free(col->devices);
}


/* Local variables:    */
/* c-basic-offset: 3   */
/* indent-tabs-mode: nil  */
/* End:          */
